<?php

namespace Cephalopod;

use NetworkIOEvent\StaticServer;
use Exception;

class Server extends StaticServer
{
    protected static array $remoteWebsiteConnections = []; // key is ID

    protected static array $browserConnections = []; // key is ID

    public static function run(string $ip, int $port, int $maxException = -1)
    {
        $exceptionHappend = 0;
        LogRecorder::logInfo("The server will listen: <{$ip}:{$port}>");
        while (true) {
            try {
                parent::run($ip, $port);
            } catch (Exception $e) {
                $exceptionHappend++;
                LogRecorder::logError('Exception in function run: ' . $e->getMessage());
                static::$remoteWebsiteConnections = [];
                static::$browserConnections = [];
                if ($maxException > -1 && $exceptionHappend > $maxException) {
                    throw $e;
                }
            }
        }
    }

    protected static function handleReadEvent(int $id, string &$readBuffer): int
    {
        static::checkIdExistsAndUniqOrThrowException($id);
        if (isset(static::$remoteWebsiteConnections[$id])) {
            $context = static::$remoteWebsiteConnections[$id];
            if ($context->peerId < 0) {
                throw new Exception("Received data from the website (ID: $id, {$context->websiteName}), but the ID of the browser corresponding to the website is less than 0, which should not happen");
            }
            if (!isset(static::$browserConnections[$context->peerId])) {
                LogRecorder::logError("Received data from the website (ID: $id, {$context->websiteName}), but the corresponding browser context (ID: {$context->peerId}) could not be found");
                return 0;
            }
            static::$browserConnections[$context->peerId]->writeBuffer .= $readBuffer;
            return strlen($readBuffer);
        }
        // else:
        $context = static::$browserConnections[$id];
        if ($context->isHttps) {
            $remoteContext = static::$remoteWebsiteConnections[$context->peerId];
            $remoteContext->writeBuffer .= $readBuffer;
            return strlen($readBuffer);
        }
        if ($context->isHttp) {
            $context->lastUnknowData .= $readBuffer;
            $remoteContext = static::$remoteWebsiteConnections[$context->peerId];
            static::cleanHttpRequestUnknowData($context->lastUnknowData, $remoteContext->writeBuffer);
            return strlen($readBuffer);
        }

        list($firstLine, $headers, $body, $unknowData, $complete) = Http::separate($readBuffer);
        if ('' === $firstLine) {
            LogRecorder::logError("Browser (ID: $id) sends data (buffer: " . strlen($readBuffer) . " bytes) but the data cannot be parsed");
            return 0;
        }
        if (!$complete) {
            return 0;
        }
        if (0 === strpos($firstLine, 'CONNECT')) {
            $context->unknowProtocol = false;
            $context->isHttps = true;
            $httpInfo = Http::parse($firstLine . Http::HTTP_EOL);
            $context->websiteName = $httpInfo['host'];
            $context->peerId = static::createConnection($httpInfo['host'], $httpInfo['port']);
            $remoteContext = new ServerContext();
            $remoteContext->websiteName = $httpInfo['host'];
            $remoteContext->isRemoteWebsite = true;
            $remoteContext->peerId = $id;
            $remoteContext->isHttps = true;
            $remoteContext->unknowProtocol = false;
            static::$remoteWebsiteConnections[$context->peerId] = $remoteContext;
            $context->writeBuffer .= "HTTP/1.1 200 Connection Established\r\n\r\n";
            $remoteContext->writeBuffer .= $unknowData;
            return strlen($readBuffer);
        }
        $context->unknowProtocol = false;
        $context->isHttp = true;
        $httpInfo = Http::parse($firstLine . Http::HTTP_EOL);
        $context->websiteName = $httpInfo['host'];
        $context->peerId = static::createConnection($httpInfo['host'], $httpInfo['port']);
        $remoteContext = new ServerContext();
        $remoteContext->websiteName = $httpInfo['host'];
        $remoteContext->isRemoteWebsite = true;
        $remoteContext->peerId = $id;
        $remoteContext->isHttp = true;
        $remoteContext->unknowProtocol = false;
        static::$remoteWebsiteConnections[$context->peerId] = $remoteContext;
        $remoteContext->writeBuffer .= Http::removeProxy($firstLine, $headers, $body);
        static::cleanHttpRequestUnknowData($unknowData, $remoteContext->writeBuffer);
        $context->lastUnknowData = $unknowData;
        return strlen($readBuffer);
    }

    protected static function handleWriteEvent(int $id, string &$writeBuffer)
    {
        static::checkIdExistsAndUniqOrThrowException($id);
        if (isset(static::$remoteWebsiteConnections[$id])) {
            $context = static::$remoteWebsiteConnections[$id];
        } else {
            $context = static::$browserConnections[$id];
        }
        $writeBuffer = $context->writeBuffer;
        $context->writeBuffer = '';
    }

    protected static function handleOpenEvent(int $id)
    {
        if (isset(static::$remoteWebsiteConnections[$id])) {
            if (!static::$remoteWebsiteConnections[$id]->isRemoteWebsite) {
                throw new Exception("ID: $id must be a website");
            }
            LogRecorder::logInfo("Successfully connected to the remote website, ID: $id, website: " . static::$remoteWebsiteConnections[$id]->websiteName);
        } else {
            if (isset(static::$browserConnections[$id])) {
                throw new Exception("Browser connection should not exist, ID: $id");
            }
            static::$browserConnections[$id] = new ServerContext();
            static::$browserConnections[$id]->isBrowser = true;
            LogRecorder::logInfo("Browser connected successfully, ID: $id");
        }
    }

    protected static function handleCloseEvent(int $id)
    {
        static::checkIdExistsAndUniqOrThrowException($id);
        if (isset(static::$remoteWebsiteConnections[$id])) {
            $context = static::$remoteWebsiteConnections[$id];
            unset(static::$remoteWebsiteConnections[$id]);
        } else {
            $context = static::$browserConnections[$id];
            unset(static::$browserConnections[$id]);
        }
        if ($context->peerId > -1) {
            static::closeConnection($context->peerId);
        }
    }

    private static function cleanHttpRequestUnknowData(string &$unknowDataOrigin, string &$writeBuffer)
    {
        $unknowData = $unknowDataOrigin;
        while(true) {
            if ('' === $unknowData) {
                break;
            }
            list($firstLine, $headers, $body, $unknowData, $complete) = Http::separate($unknowData);
            if (!$complete) {
                $unknowDataOrigin = $unknowData;
                break;
            }
            $writeBuffer .= Http::removeProxy($firstLine, $headers, $body);
        }
    }

    private static function checkIdExistsAndUniqOrThrowException(int $id)
    {
        if (!isset(static::$browserConnections[$id]) && !isset(static::$remoteWebsiteConnections[$id])) {
            throw new Exception("Unknow ID: $id");
        }
        if (isset(static::$browserConnections[$id]) && isset(static::$remoteWebsiteConnections[$id])) {
            throw new Exception("Not unique ID: $id");
        }
    }
}
